using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using Iivil.Infrastructure.Collections.Generic;
using Iivil.Infrastructure.Logging;
using Iivil.Infrastructure.Logging.Providers;

namespace Iivil.Powershell.Debugging
{
    public partial class TraceView : Form
    {
        private const string prompt = "PS> ";
        private List<PSObject> _objects;
        private Dictionary<PSObject, ObjectAction> _actionsTaken;
        private TraceStream _stream;
        private delegate void ProcessObjectAsyncDelegate(PSObject obj);
        private delegate void InvokeDelegate<T>(T obj);
        private delegate void ProcessAsyncDelegate();

        private ManualResetEvent _pipelineExecutionThreadLock;
        private ProcessingState _state = ProcessingState.Busy;
        private Runspace _runspace;
        private RunspaceInvoke _runspaceInvoker;

        private enum ProcessingState
        {
            Busy,
            Ready,
            Idle,
            Running,
            Completed
        }

        private void SetState(ProcessingState newState)
        {
            if (newState == _state)
            {
                LogProvider.WriteWarning("TraceView", "State changed from {0} to {1}", _state, newState);
                return;
            }
            LogProvider.WriteDebug("TraceView", "State changed from {0} to {1}", _state, newState);
            _state = newState;
            SetStatus(newState.ToString());
        }

        private void ReleasePipelineThreadLock()
        {
            _pipelineExecutionThreadLock.Set();
        }

        private void WaitAndResetPipelineThreadLock()
        {
            _pipelineExecutionThreadLock.WaitOne();
            _pipelineExecutionThreadLock.Reset();
        }


        private bool FollowStream
        {
            get
            {
                return btnFollowStream.Checked;
            }
        }


        public TraceView(TraceStream stream)
        {
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "View loading...");
            Application.EnableVisualStyles();
            InitializeComponent();
            _objects = new List<PSObject>();
            _actionsTaken = new Dictionary<PSObject, ObjectAction>();
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Setting up event handlers...");
            _pipelineExecutionThreadLock = new ManualResetEvent(false);
            _stream = stream;
            _stream.ProcessingStarted += new TraceStream.ProcessStateChangedDelegate(ProcessingStarted);
            _stream.ProcessObject += new TraceStream.ProcessObjectDelegate(ProcessObject);
            _stream.ProcessingFinished += new TraceStream.ProcessStateChangedDelegate(ProcessingFinished);


        }

        private void ProcessingFinished()
        {
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Event raised: ProcessingFinished");
            this.BeginInvoke(new ProcessAsyncDelegate(ProcessingFinishedAsync));
            if (_stream.ProcessingMode == ProcessingMode.Collect)
            {
                LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Waiting for user action...");
                SetState(ProcessingState.Idle);
                WaitAndResetPipelineThreadLock();
            }
        }

        private void ProcessingFinishedAsync()
        {
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "ProcessingFinishedAsync invoked.");

            btnSkip.Enabled = (_stream.ProcessingMode == ProcessingMode.Collect);
            btnPassthrough.Enabled = (_stream.ProcessingMode == ProcessingMode.Collect);
            btnRelease.Enabled = (_stream.ProcessingMode == ProcessingMode.Collect);
            btnDiscard.Enabled = (_stream.ProcessingMode == ProcessingMode.Collect);
            SetState(ProcessingState.Completed);
        }


        private ObjectAction ProcessObject(PSObject obj)
        {
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Event raised: ProcessObject");
            SetState(ProcessingState.Running);
            _objects.Add(obj);
            _actionsTaken.Add(obj, ObjectAction.Passthrough);
            this.BeginInvoke(new ProcessObjectAsyncDelegate(ProcessObjectAsync), obj);
            //exec trace
            if (textTraceScriptBlock.Text.Trim().Length > 0)
            {
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
                LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Executing trace function on object...");

                _runspace.SessionStateProxy.SetVariable("_", obj);
                using (Pipeline formatter = _runspace.CreatePipeline())
                {  
                    formatter.Commands.AddScript(textTraceScriptBlock.Text.Trim(), true);
                    //formatter.Commands[0].Parameters.Add("_", obj); // this didnt work, how do you add a local variable?
                    formatter.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
                    formatter.Commands.Add("out-string");
                    Collection<PSObject> output = formatter.Invoke();
                    StringBuilder traceOut = new StringBuilder();
                    foreach (PSObject o in output)
                        traceOut.AppendLine(o.ToString());

                   // int headerWith = 120; make header all fancy and stuff, make it a ps function?
                   
                    string traceHeader = String.Format("######################## TRACE ({0}) ########################", obj);
                    string traceBody = traceOut.ToString().Trim();
                    string traceMessage = string.Format("{0}{0}{1}{0}{0}{2}{0}", Environment.NewLine, traceHeader, traceBody);
                    
                    this.Invoke(new InvokeDelegate<string>(this.WriteTrace), traceMessage);
                    
                }
                _runspace.SessionStateProxy.SetVariable("_", null); //clear var?
                stopwatch.Stop();
                LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Trace executed in {0:0.000}", stopwatch.Elapsed.TotalSeconds);

            }
            if (_stream.ProcessingMode == ProcessingMode.Step)
            {
                LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Waiting for user action...");
                SetState(ProcessingState.Idle);
                WaitAndResetPipelineThreadLock();
            }
            return _actionsTaken[obj];
        }

        private void WriteTrace(string obj)
        {
            textTraceOutput.AppendText(obj);
            if (FollowStream)
            {
                textTraceOutput.SelectionStart = textTraceOutput.Text.Length;
                textTraceOutput.SelectionLength = 0;
                textTraceOutput.ScrollToCaret();
            }
        }


        private void ProcessObjectAsync(PSObject obj)
        {
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "ProcessObjectAsync invoked.");

            int index = listObjects.Items.Add(obj);
            if (FollowStream)
            {
                listObjects.SelectedIndex = index;
            }
            if (_stream.ProcessingMode == ProcessingMode.Step)
            {
                btnPassthrough.Enabled = true;
                btnSkip.Enabled = true;
            }
            //textTraceOutput.AppendText(Environment.NewLine);
            StatusLabel.Text = "Count: " + _objects.Count.ToString();
        }

        private void ProcessingStarted()
        {
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Event raised: ProcessingStarted");
            SetState(ProcessingState.Running);
            if (!_stream.Run.ToBool())
            {
                LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Waiting for user action...");
                WaitAndResetPipelineThreadLock();
            }
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Creating PowerShell Runspace...");
            RunspaceConfiguration config = RunspaceConfiguration.Create();
            _runspace = RunspaceFactory.CreateRunspace(_stream.Host, config);
            _runspace.Open();
            _runspaceInvoker = new RunspaceInvoke(_runspace);
            this.BeginInvoke(new ProcessAsyncDelegate(ProcessingStartedAsync));
        }

        private void ProcessingStartedAsync()
        {
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "ProcessingStartedAsync invoked.");
            if (_stream.Run)
            {
                comboMode.Enabled = false;
            }
            btnTrace.Enabled = false;
        }

        internal void WriteDebug(DateTime time, Severity severity, string category, string message)
        {
            if (severity == Severity.Progress)
                lblLastProgressMessage.Text = message;

            if (!_stream.LogEnabled(severity))
                return;
            string logMessage = String.Format("{2} @ {0:HH:mm:ss}\t{3}\r\n", time, severity, category, message);
            textDebug.SelectionStart = textDebug.TextLength;
            switch (severity)
            {
                case Severity.Progress:
                    textDebug.SelectionColor = Color.White;
                    break;
                case Severity.Warning:
                    textDebug.SelectionColor = Color.Yellow;
                    break;
                case Severity.Error:
                    textDebug.SelectionColor = Color.Red;
                    break;
                case Severity.Debug:
                    textDebug.SelectionColor = Color.Green;
                    break;

            }
            textDebug.SelectedText = logMessage;
            textDebug.ScrollToCaret();
        }

        private void TraceView_Load(object sender, EventArgs e)
        {
            comboMode.SelectedIndex = (int)_stream.ProcessingMode;
            btnFollowStream.Checked = !_stream.NoFollow.ToBool();
            btnAlwaysOnTop.Checked = _stream.OnTop.ToBool();
            this.TopMost = btnAlwaysOnTop.Checked;
            if (_stream.ForEach != null) textTraceScriptBlock.Text = _stream.ForEach.ToString();


            debugToolStripMenuItem1.Checked = _stream.LogEnabled(Severity.Debug);
            progressToolStripMenuItem1.Checked = _stream.LogEnabled(Severity.Progress);
            warningToolStripMenuItem1.Checked = _stream.LogEnabled(Severity.Warning);
            errorToolStripMenuItem1.Checked = _stream.LogEnabled(Severity.Error);

            if (!_stream.LogEnabled())
            {
                tabSouth.TabPages.Remove(tpDebugOutput);
            }
        }

        private void runToolStripMenuItem_Click(object sender, EventArgs e)
        {
            comboMode.Enabled = false;
            ReleasePipelineThreadLock();
        }

        private void listObjects_SelectedIndexChanged(object sender, EventArgs e)
        {
            ShowObject(_objects[listObjects.SelectedIndex]);
        }

        private void ShowObject(PSObject obj)
        {
            if (_stream.ProcessingMode == ProcessingMode.Step || _state == ProcessingState.Completed)
            {
                propObject.SelectedObject = obj.BaseObject;
                //extract all types from this object
                comboObjectTypes.Items.Clear();
                Dictionary<Type, Set<Type>> typeInterfaces = new Dictionary<Type, Set<Type>>();
                Set<Type> intefaceImplementedByBaseClass = new Set<Type>();
                for (int i = obj.TypeNames.Count - 2; i >= 0; i--)
                {
                    Type t = Type.GetType(obj.TypeNames[i]);
                    Type[] interfaces = t.GetInterfaces();
                    typeInterfaces.Add(t, (new Set<Type>(interfaces)).Difference(intefaceImplementedByBaseClass));
                    intefaceImplementedByBaseClass.AddRange(interfaces);
                }

                foreach (string typeName in obj.TypeNames)
                {
                    string suffix = string.Empty;
                    Type t = Type.GetType(typeName);
                    if (typeInterfaces.ContainsKey(t) && typeInterfaces[t].Count > 0)
                    {
                        suffix = string.Format(" ({0})", string.Join(", ", Array.ConvertAll<Type, string>(typeInterfaces[t].ToArray(),
                                                                                                          delegate(Type inputType) { return inputType.ToString(); })));
                    }

                    comboObjectTypes.Items.Add(typeName + suffix);
                }

                comboObjectTypes.SelectedIndex = 0;

                comboObjectTypes.Enabled = true;
                propObject.Enabled = true;
            }
            else
            {
                comboObjectTypes.Enabled = false;
                propObject.Enabled = false;
            }


        }

        private void btnAlwaysOnTop_Click(object sender, EventArgs e)
        {
            this.TopMost = btnAlwaysOnTop.Checked;

        }

        private void comboMode_SelectedIndexChanged(object sender, EventArgs e)
        {
            _stream.ProcessingMode = (ProcessingMode)comboMode.SelectedIndex;

            btnPassthrough.Visible = _stream.ProcessingMode != ProcessingMode.Stream;
            btnSkip.Visible = _stream.ProcessingMode != ProcessingMode.Stream;
            btnRelease.Visible = _stream.ProcessingMode == ProcessingMode.Collect;
            btnDiscard.Visible = _stream.ProcessingMode == ProcessingMode.Collect;
        }

        private void TraceView_Shown(object sender, EventArgs e)
        {
            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "Attaching log writer...");
            UserLogProvider ulw = (UserLogProvider)LogProvider.Instance;
            ulw.LogWriteEvent =
                delegate(DateTime time, Severity severity, string category, string message)
                {
                    this.BeginInvoke(new UserLogProvider.LogWriteEventHandler(WriteDebug), time, severity,
                                     category, message);
                };

            LogProvider.WriteDebug(Thread.CurrentThread.Name + "::TraceView", "View loaded, releasing Pipeline Execution Thread...");
            SetState(ProcessingState.Ready);
            _stream.ViewReadyForProcessing.Set();
        }


        public void SetStatus(string status)
        {
            lblCurrentStatus.Text = status;
        }

        private void btnPassthrough_Click(object sender, EventArgs e)
        {
            ReleaseObject(ObjectAction.Passthrough);
            listObjects.Invalidate();
        }

        private void ReleaseObject(ObjectAction action)
        {
            _actionsTaken[_objects[_objects.Count - 1]] = action;
            _pipelineExecutionThreadLock.Set();
        }

        private void btnSkip_Click(object sender, EventArgs e)
        {
            ReleaseObject(ObjectAction.Skip);
            listObjects.Invalidate();
        }

        private void listObjects_DrawItem(object sender, DrawItemEventArgs e)
        {
            if (e.Index == -1)
                return;
            using (Font italic = new Font(e.Font, FontStyle.Strikeout))
            {
                listObjects.DrawMode = DrawMode.OwnerDrawFixed;
                // Draw the background of the ListBox control for each item.
                e.DrawBackground();
                // Define the default color of the brush as black.
                Brush myBrush = Brushes.Black;
                // Draw the current item text based on the current Font and the custom brush settings.
                Font f = (_actionsTaken[_objects[e.Index]] == ObjectAction.Passthrough ? e.Font : italic);
                PSObject obj = (PSObject)listObjects.Items[e.Index];


                string output;
                if (obj.Properties["Name"] != null)
                {
                    output = (string)obj.Properties["Name"].Value;
                }
                else
                    output = obj.ToString();
                e.Graphics.DrawString(output, f, myBrush, e.Bounds, StringFormat.GenericDefault);
                // If the ListBox has focus, draw a focus rectangle around the selected item.
                if (listObjects.Focused)
                    e.DrawFocusRectangle();
            }
        }

        private void listObjects_DoubleClick(object sender, EventArgs e)
        {
            if (listObjects.SelectedIndex > -1)
            {
                Runspace rs = RunspaceFactory.CreateRunspace(_stream.Host);

                Pipeline foreachPipeline;
                //if (rs.RunspaceStateInfo.State == RunspaceState.Closed)
                //FormatDefaultCommand 
                textImmediate.AppendText("invoke-trace $_" + Environment.NewLine);
                foreachPipeline = rs.CreatePipeline(textTraceScriptBlock.Text, false);
                rs.Open();
                rs.SessionStateProxy.SetVariable("_", ((PSObject)listObjects.SelectedItem).BaseObject);
                try
                {
                    Collection<PSObject> result = foreachPipeline.Invoke();
                    foreach (PSObject o in result)
                    {
                        if (o != null)
                            textImmediate.AppendText(o.ToString() + Environment.NewLine);
                    }
                }
                catch (Exception err)
                {
                    _stream.Host.UI.WriteErrorLine(err.Message);
                }
                finally
                {
                    rs.CloseAsync();
                }

                foreachPipeline.Dispose();
                rs.Dispose();

                textImmediate.AppendText(prompt);
                textImmediate.SelectionStart = textImmediate.TextLength;
                textImmediate.ScrollToCaret();
            }
        }


        private void textImmediate_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
        {


        }

        private void allToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            errorToolStripMenuItem1.Checked = true;
            warningToolStripMenuItem1.Checked = true;
            progressToolStripMenuItem1.Checked = true;
            debugToolStripMenuItem1.Checked = true;
        }

        private void noneToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            errorToolStripMenuItem1.Checked = false;
            warningToolStripMenuItem1.Checked = false;
            progressToolStripMenuItem1.Checked = false;
            debugToolStripMenuItem1.Checked = false;
        }

        private void textImmediate_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyData == Keys.Tab)
            {
                e.SuppressKeyPress = true;
                // TODO just call let Function:tabexpansion do all the heavy lifting, 
                //      get in touch with with WOW/MOV whatever his nick was and see 
                //      if he would like to contribute his, it seemed decent?
                MessageBox.Show("TabExpansion");
            }
            else if (e.KeyData == Keys.Enter)
            {
                e.SuppressKeyPress = true;
                MessageBox.Show("Execute");
                textImmediate.AppendText(prompt);
                textImmediate.SelectionStart = textImmediate.TextLength;
                textImmediate.ScrollToCaret();

            }
        }

        private void textImmediate_KeyPress(object sender, KeyPressEventArgs e)
        {

        }

        private void textTraceOutput_TextChanged(object sender, EventArgs e)
        {

        }

        private void TraceView_FormClosed(object sender, FormClosedEventArgs e)
        {
            _runspaceInvoker.Dispose();
            _runspace.Dispose();
        }







    }
}